package com.harth.blemultiplatform.ble.core

import kotlin.time.Duration

/**
 * Connection state of a Bluetooth LE peripheral.
 */
sealed class ConnectionState {

    /**
     * The client is trying to connect to the peripheral.
     *
     * When connected, the state will change to [Connected].
     *
     * On timeout or cancellation, the state will change to [Disconnected] with the correct reason.
     */
    data object Connecting: ConnectionState()

    /**
     * The peripheral is connected.
     *
     * When the connection terminates, the state will change to [Disconnected]
     * or [Connecting] if the client will try to reconnect.
     */
    data object Connected: ConnectionState()

    /**
     * Disconnection has been initiated.
     *
     * This state is set when the `disconnect()` method is called on the peripheral.
     *
     * This is followed by [Disconnected] state with reason [Success][Disconnected.Reason.Success].
     */
    data object Disconnecting: ConnectionState()

    /**
     * Device has disconnected.
     *
     * If the [reason] is null, it means that no connection attempt was made. This is
     * the initial state of the peripheral.
     *
     * Note, that it doesn't mean that the peripheral is not connected, it may be connected
     * using another [Manager].
     *
     * @param reason Reason of disconnection or `null` if the connection was never initiated.
     * This is the initial state of the peripheral.
     */
    data class Disconnected(val reason: Reason? = null): ConnectionState() {

        /** Reason of disconnection. */
        sealed class Reason {
            /** The disconnection was initiated by the user.  */
            data object Success: Reason()
            /**
             * Unknown error.
             *
             * @property status The status code returned by the Bluetooth stack.
             */
            data class Unknown(val status: Int): Reason()
            /** The local device initiated disconnection.  */
            data object TerminateLocalHost: Reason()
            /** The remote device initiated graceful disconnection. */
            data object TerminatePeerUser: Reason()
            /**
             * Insufficient authentication.
             *
             * This error is returned when the Android device tries to enable security on a
             * device that had its bond information removed. Remove bond information on the phone
             * and retry.
             */
            data object InsufficientAuthentication: Reason()
            /** The device got out of range or has turned off. */
            data object LinkLoss: Reason()
            /** Connection attempt was cancelled.  */
            data object Cancelled: Reason()
            /**
             * Connection attempt was aborted due to an unsupported address.
             *
             * Resolvable Private Address (RPA) can rotate, causing address to "expire" in the
             * background connection list. RPA is allowed for direct connection, as such request
             * times out after a short period of time.
             *
             * See: [Source code](https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Bluetooth/system/stack/gatt/gatt_api.cc;l=1498)
             */
            data object UnsupportedAddress: Reason()
            /**
             * Connection attempt was aborted due PHY negotiations failure.
             *
             * Some phones fail to respond properly to a PHY request sent immediately after establishing
             * the connection (i.e. Samsung S8 with Android 9 responds with Instant from the past).
             * The peripheral drops the connection when a valid response is not received before
             * the timeout. A solution may be to remove the PHY LE2M request from the peripheral side,
             * i.e. by setting `CONFIG_BT_CTLR_PHY_2M=n` in Zephyr.
             *
             * This bug seems to be non-existent on other Android devices we tested.
             */
            data object UnsupportedConfiguration: Reason()
            /**
             * The connection attempt timed out.
             *
             * The device might have reboot, is out of range, turned off  or doesn't respond
             * for another reason.
             *
             * @property duration The duration of the timeout.
             */
            data class Timeout(val duration: Duration): Reason()
        }

        /**
         * A quick check whether the disconnection was initiated by the user.
         *
         * This returns `false` in the initial state, that is before the connection attempt was started.
         */
        val isUserInitiated: Boolean
            get() = reason is Reason.Success || reason is Reason.Cancelled
    }

    /** Whether the connection is open. */
    val isConnected: Boolean
        get() = this is Connected

    /** Whether the connection closed or getting closed. */
    val isDisconnected: Boolean
        get() = this is Disconnected
}